Исследование по разработке стратегии взаимодействия с клиентами сети фитнес-центраов¶

Задачи исследования - на основе данных о посетителях:

  1. спрогнозировать их отток и проанализировать основные признаки, наиболее сильно на него влияющие;
  2. сформировать типичные портреты клиентов;
  3. разработать рекомендации по повышению качества работы с клиентами.

Навигация:

  • Описание и задачи
  • Исследовательский анализ данных
    • Распределение признаков
    • Корреляции признаков
  • Модель прогнозирования оттока клиентов
  • Кластеризация клиентов
  • Выводы и рекомендации

Импортируем необходимые библиотеки.

In [1]:
import pandas as pd
import phik

import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score

from sklearn.preprocessing import StandardScaler

from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import KMeans

Исследовательский анализ данных¶

к навигации

Загрузим данные и ознакомимся с ними.

In [2]:
local_path = r'C:\Практикум\Учебные проекты\13 Основы машинного обучения\\'
cloud_path = r'Путь к облачному хранилищу'

def get_data(file_name):
    try:
        data = pd.read_csv(local_path + file_name)
        print('Датасет {0} загружен локально.'.format(file_name))
    except:
        data = pd.read_csv(cloud_path + file_name)
        print('Датасет {0} загружен из облака.'.format(file_name))
    return data

data = get_data('gym_churn.csv')
Датасет gym_churn.csv загружен локально.
In [3]:
#data.info()
#data.head()

Все данные (в том числе, категориальные) представлены в числовом формате, пропуски отсутствуют.

In [4]:
data.duplicated().sum()
Out[4]:
0

Явных дубликатов нет.

Предобработка не требуется.

Распределение признаков¶

к навигации

Посмотрим на средние значения параметров для оттекших и оставшихся клиентов.

In [5]:
data.groupby('Churn').agg('mean')
Out[5]:
gender Near_Location Partner Promo_friends Phone Contract_period Group_visits Age Avg_additional_charges_total Month_to_end_contract Lifetime Avg_class_frequency_total Avg_class_frequency_current_month
Churn
0 0.510037 0.873086 0.534195 0.353522 0.903709 5.747193 0.464103 29.976523 158.445715 5.283089 4.711807 2.024876 2.027882
1 0.510839 0.768143 0.355325 0.183789 0.902922 1.728558 0.268615 26.989632 115.082899 1.662582 0.990575 1.474995 1.044546

Некоторые значения (такие как - пол, наличие контактного телефона, etc.) практически равны. Некоторые (например, длительность абонемента) заметно отличаются. Очевидно, что факторы влияют на отток клиентов по-разному.

Построим графики распределения каждого параметра для обеих групп.

In [6]:
print("Распределение параметров для оставшихся (Churn = 0) и ушедших (Churn = 1) клиентов")
for feature in data.columns:
    fig = px.histogram(data, x=feature, color='Churn', barmode='group', width=600, height=300)
    fig.show()
Распределение параметров для оставшихся (Churn = 0) и ушедших (Churn = 1) клиентов
  • Распределение (соотношение) признаков 'gender' и 'Phone' в группах оттекших и оставшихся примерно одинаково. Вероятно, эти признаки не влияют на отток.
  • Среди оставшихся заметно больше доля живущих/работающих рядом; работающих в организации-партнере; пришедших по рекомендации друзей; посещающих групповые занятия.
  • Ощутима разница между группами по признаку 'Contract_period'. Соотношение оставшихся/ушедших для 1-месячного договора: 4/3, 6-месячного: 7/1, 12-месячного: 37/1. Видимо, срок контракта сильно влияет на целевой признак.
  • Распределение возраста в обеих группах похоже на нормальное. Аномалий не видно. Для ушедших распределение смещено влево, в сторону молодости.
  • Распределение параметра 'Month_to_end_contract' логично согласуется с распределением 'Contract_period'.
  • Гистограмма признака 'Lifetime' похожа на распределение Пуассона. Аномалий не видно. У ушедших этот параметр ощутимо ниже.
  • Распределения 'Avg_class_frequency_total' и 'Avg_class_frequency_current_month' похожи на смещенные влево нормальные. Для второго признака степень смещения ушедших заметнее. Видимо, перед тем как уйти, они начинают ходить реже. Виден пик в районе нуля - те, кто купили абонемент и не ходят. Любопытно, что на первом графике доля неходящих оставшихся больше, а на втором - меньше неходящих и ушедших.

Корреляции признаков между собой и целевой переменной.¶

к навигации

Построим тепловую карту корреляций для категориальных переменных (phik_matrix).

In [7]:
fig, ax = plt.subplots(figsize=(8,7))
sns.heatmap(data[['gender', 
                  'Near_Location', 
                  'Partner', 
                  'Promo_friends', 
                  'Phone', 
                  'Group_visits',
                  'Churn']].phik_matrix(), annot=True, linewidth=.1) \
   .set(title='Тепловая карта корреляций для категориальных переменных')
plt.show()
interval columns not set, guessing: ['gender', 'Near_Location', 'Partner', 'Promo_friends', 'Phone', 'Group_visits', 'Churn']
No description has been provided for this image
  • Работа в компании-партнере и использование промокода при записи показывают умеренную корреляцию между собой.
  • Ни один из параметров не показывает даже умеренной связи с целевой переменной.
  • Пол и наличие контактных данных вообще никак ни с чем не коррелирут.

Построим тепловую карту корреляций Пирсона/Спирмана для количественных переменных.

In [8]:
fig, ax = plt.subplots(figsize=(9,8))
sns.heatmap(data[['Age',
                  'Lifetime',
                  'Contract_period',
                  'Month_to_end_contract',
                  'Avg_class_frequency_total',
                  'Avg_class_frequency_current_month',
                  'Avg_additional_charges_total',
                  'Churn']].corr(), annot=True, linewidth=.1, cmap='coolwarm') \
   .set(title='Тепловая карта корреляций для количественных переменных')
plt.show()
No description has been provided for this image

Очень сильную положительную корреляцию между собой демонстрируют:

  • длительность абонемента и срок до его окончания;
  • средняя частота посещений за все время и за последний месяц.
    Довольно логично.
    Остальные факторы слабо (или очень слабо) положительно коррелируют друг с другом.

Некоторые параметры умеренно отрицательно коррелируют с целевой переменной. Вероятность оттока тем меньше чем:

  • старше клиент (понимание приходит с опытом);
  • больше времени прошло с момента первого обращения в фитнес-центр (постоянные клиенты, без иронии - такие постоянные!);
  • длиннее абонемент и
  • дольше до его окончания (заплатил - ходи!);
  • чаще клиент посещал занятия в последний месяц (бросают поэтапно, а не в один момент).
    Средняя посещаемость за весь срок и суммарная побочная выручка не сильно влияют на отток (-0.25 и -0.2).

Модель прогнозирования оттока клиентов¶

к навигации

Обучим несколько моделей прогнозирования отткока клиентов и сравним результаты их предсказательной способности, используя метрики Accuracy, Precision и Recall. Обочим модели:

  • методами логистической регрессии и случайного леса;
  • на сырых и очищенных данных;
  • на сырых и стандартизированных данных.
    (всего - восемь комбинаций)

Объявим функцию,

  • разбивающую данные на обучающую и тестовую выборки,
  • стандартизирующую данные,
  • обучающую модель
  • и получающую интересующие нас метрики.
In [9]:
def get_models_n_metrics(data, scaled, method, name):

    # Разобъем данные на обучающую и валидационную выборки в соотношении 3/1:
    X = data.drop('Churn', axis=1)
    y = data['Churn']
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
    
    # Стандартизируем данные:
    if scaled == True:
        ss = StandardScaler()
        X_train = ss.fit_transform(X_train)
        X_test = ss.transform(X_test)

    # Обучим модель:
    model = method
    model.fit(X_train, y_train)
    prediction = model.predict(X_test)

    # Получим метрики Accuracy, Precision и Recall:
    accuracy = accuracy_score(y_test, prediction)
    precision = precision_score (y_test, prediction)
    recall = recall_score (y_test, prediction)

    result = pd.DataFrame(data={'accuracy': accuracy, 'precision': precision, 'recall': recall}, index=name)
    return result

Создадим датафрейм, из которого исключим сильно коррелирующие между собой признаки, чтобы избавиться от мультиколлениарности, а также - параметры, не влияющие на целевую переменную.

In [10]:
r_data = data
c_data = data.drop(['Month_to_end_contract', 'Avg_class_frequency_total', 'gender', 'Phone'], axis=1)

Получим таблицу с показателями метрик для каждого из вариантов моделей.

In [11]:
metrics = pd.DataFrame(columns = ['accuracy', 'precision', 'recall'])

for key, value in {'Логистическая регрессия, ': LogisticRegression(solver='liblinear'),
                   'Случайный лес, ': RandomForestClassifier()}.items():
    name_1 = key
    method = value
    for key, value in {'сырые ': r_data, 'очищенные ': c_data}.items():
        name_2 = name_1 + key
        data = value
        for key, value in {'стандартизированные данные': True, 'нестандартизированные данные': False}.items():
            name = name_2 + key
            scaled = value
            metrics = metrics.append(get_models_n_metrics(data, scaled, method, [name]))

# Добавим поле со средним значением:
metrics['avg_metrics'] = metrics.mean(axis=1)

Построим тепловую карту сравнительной таблицы.

In [12]:
fig, ax = plt.subplots(figsize=(8,6))
sns.heatmap(metrics, annot=True, linewidth=.1, cmap='Greens') \
   .set(title='Сравнение метрик для обученных моделей')
plt.show()
No description has been provided for this image
  • Очистка данных негативно отразилась на всех метриках.
  • Стандартизация почти не повлияла на результаты.
  • Метод логистической регрессии показал себя немного лучше по количеству правильных ответов, точности и полноте.
  • Все модели продемонстрировали сравнимые хорошие показатели.
    Результаты теста могут различаться при ином разбиении на выборки.

Кластеризация клиентов¶

к навигации

Построим дендрограмму, которая поможет определить количество кластеров, осуществим кластеризацию, и постараемся выделить ключевые признаки для каждого кластера.

Исключим никак ни с чем не коррелирующие параметры.

In [13]:
data = r_data.drop(['gender', 'Phone'], axis=1)

Обучим модель на генеральной выборке без столбца с оттоком и стандартизируем данные.

In [14]:
X = data.drop('Churn', axis=1)
scaler = StandardScaler()
X_st = scaler.fit_transform(X)

Построим дендрограмму.

In [15]:
linked = linkage(X_st, method = 'ward')
plt.figure(figsize=(15, 15))  
dendrogram(linked, orientation='top')
plt.title('Иерархическая кластеризация для сети фитнес-центров')
plt.show() 
No description has been provided for this image

Основываясь на уровне ветвения и размере, кажется целесообразным выделить 5 кластеров, а не 3, как предлагает алгоритм.

In [16]:
km = KMeans(n_clusters=5, random_state=0)
labels = km.fit_predict(X_st)

Добавим к датафрейму столбец с номером кластера и посмотрим на сгруппированные данные.

In [17]:
data['claster'] = labels
data.groupby('claster').agg('mean').sort_values('Churn').round(2)
Out[17]:
Near_Location Partner Promo_friends Contract_period Group_visits Age Avg_additional_charges_total Month_to_end_contract Lifetime Avg_class_frequency_total Avg_class_frequency_current_month Churn
claster
3 0.94 0.74 0.49 11.91 0.55 29.91 164.41 10.91 4.68 1.99 1.98 0.02
1 0.97 0.27 0.09 2.93 0.47 30.25 163.54 2.69 5.23 2.90 2.90 0.06
0 1.00 0.82 1.00 3.14 0.45 29.22 141.79 2.90 3.73 1.75 1.64 0.25
4 0.00 0.47 0.08 2.21 0.22 28.47 133.48 2.08 2.77 1.65 1.46 0.45
2 1.00 0.24 0.02 1.96 0.33 28.19 130.88 1.88 2.38 1.29 1.05 0.53

Значения в столбцах отличаются, можно выделить характерные черты для каждого кластера.
Исключение - возраст. Исключим столбец 'Age' и пересоберем кластеры.

In [18]:
data_1 = r_data.drop(['gender', 'Phone', 'Age'], axis=1)
X_1 = data_1.drop('Churn', axis=1)

X_st_1 = scaler.fit_transform(X_1)

km = KMeans(n_clusters=5, random_state=0)
labels_1 = km.fit_predict(X_st_1)

data_1['claster'] = labels_1

clasters = data_1.groupby('claster').agg('mean').sort_values('Churn')
In [19]:
clasters.round(2)
Out[19]:
Near_Location Partner Promo_friends Contract_period Group_visits Avg_additional_charges_total Month_to_end_contract Lifetime Avg_class_frequency_total Avg_class_frequency_current_month Churn
claster
1 0.94 0.74 0.49 11.90 0.55 164.87 10.90 4.70 1.99 1.98 0.02
4 0.97 0.25 0.08 2.89 0.47 161.47 2.65 5.13 2.89 2.89 0.07
2 1.00 0.80 1.00 3.12 0.45 140.85 2.89 3.66 1.75 1.64 0.25
0 0.00 0.47 0.08 2.18 0.21 133.56 2.04 2.77 1.66 1.47 0.45
3 1.00 0.25 0.01 1.95 0.33 131.69 1.86 2.41 1.27 1.03 0.52

Средние значения параметров не сильно изменились (хотя, заранее мы этого не знали и шаг кажется оправданным).
Для упрощения анализа ранжируем распределение значений признаков от 1-го до 10-ти, где единице соответствует минимальное значение и построим тепловую карту.

In [20]:
n = 10
group_names = list(range(1, n+1))
for column in clasters.columns:
    clasters[column] = pd.cut(clasters[column], bins = n, labels=group_names)
clasters = clasters.astype('int')
In [21]:
fig, ax = plt.subplots(figsize=(15,5))
sns.heatmap(clasters, linewidth=.1, annot=True) \
   .set(title='Распределение признаков по кластерам')
plt.show()
No description has been provided for this image

Расположим кластеры в порядке увеличения вероятности оттока и выделим характерные черты:

  1. Кластер "1": С большой вероятностью - сотрудник компании-партнера и/или использовавший промокод при оплате первого абонемента, с очень долгим контрактом. Постоянный клиент. Ходит два раза в неделю, не редко посещает групповые занятия. Вероятность оттока - около 2%.
  2. Кластер "4": Характерная черта - максимальная среди всех кластеров частота посещений - 3 раза в неделю, при сравнительно небольшом сроке контракта. Также - постоянный клиент. Вероятность оттока - 7%.
  3. Кластер "2": Использовавший промокод сотрудник компании-партнера. Почти все стальные показатели близки к средним. Вероятность оттока - 25%.
  4. Кластер "0": Далеко живущие. Ходят полтора раза в неделю, и этот показатель падает. Почти не посещают групповых занятий. Вроятность оттока 45%.
  5. Кластер "3": Живут рядом, но ходят редко и имеют краткосрочные контракты. Возможно - зашли попробовать, но не очень понравилось. Уходят в более чем половине случаев.

Выводы и рекомендации¶

к навигации

В ходе предварительного анализа данных выяснилось, что предобработка не требуется.
Построение тепловых карт корреляций показало неравнозначное влияние факторов на целевую переменную, а также, на взаимосвязь параметров между собой.
В качестве метода разбиения на обучающую и валидационную выборки был выбран случайный метод, т.к. данные не содержат временных рядов.
Были обучены и протестированы две модели прогнозирования оттока клиентов. Использовался алгоритм логической регрессии и случайный лес. Обе модели показали высокое значение метрик Accuracy, Precision и Recall.
Удалось разбить клиентов на группы с помощью алгоритма кластеризации. Ниже представлены маркетинговые рекомендации, как по каждой группе, так и общие.

  • "1". Группа клиентов с долгосрочным контрактом не требует внимания прямо сейчас, но, возможно, есть смысл постоянно поддерживать их контракт долгим. Например, предлагая льготные условия пролонгации. И чем заранее, тем льготнее.
  • "4". Данная группа, вероятно, не требует дополнительных трат маркетингового бюджета, однако, имеет смысл провести дополнительное исследование и выявить факторы, которые сильнее влияют на отток в этой группе.
  • "2". За исключением короткого абонемента и довольно большой вероятностью оттока, эта группа близка по характеристикам к первой. Возможно, жесткая, но действенная мера - ограничение минимального срока договора для сотрудников компаний-партнеров - могла бы способствовать переходу этого типа клиентов в первый кластер.
  • "0". Вряд ли что-либо возможно сделать с основным показателем для этой группы - расстоянием от работы или дома до фитнес-центра, однако, редкость посещения групповых занятий (что, скорее - следствие) подвластна влиянию. Возможно, если предложить особые условия посещения таких занятий, клиенты из этой группы найдут себе фитнес-друзей, будут чаще и с большим интересом ходить в фитнес-центр, что может снизить показатель оттока.
  • "3". Пришли попробовать, но не понравилось. Нужно сделать так, чтобы понравилось. В отличие от предыдущей группы, на эту, кажется, проще повлиять маркетинговым способом. Можно детально поработать с такими клиентами, провести, например, анкетирование, и выяснить, чем их можно заинтересовать.

Для всех групп: кажется, самым влиятельным, и, в то же время, подверженным влиянию фактором является наличие действующего долгосрочного договора. Имеет смысл детально проработать стратегию мотивирования клиентов к заключению такого типа контрактов.